/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.wider.foundation.channel;

import com.wider.foundation.ArrayX;
import com.wider.foundation.Buffered;
import com.wider.foundation.BufferingShim;
import com.wider.foundation.DLogger;
import com.wider.foundation.Named;
import com.wider.foundation.Numbering;
import com.wider.foundation.concurrency.AdaptiveAgedExecutorPool;
import com.wider.foundation.concurrency.Cancellable;
import com.wider.foundation.concurrency.CountedBlockingQueue;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;

/**
 * Utility class for user to construct a HTTP client which has the ability of performing 
 * the GET and POST operation.
 *
 * This class defines a Buffered(Multiplexed) communication channel towards a specified
 * HTTP service.
 * 
 * Multiple asynchronous requests are allowed to be transmitted through this channel.
 *
 * Before actually performing the GET and POST operation, 
 * <code>init(AdaptiveAgedExecutorPool pool)</code> or
 * <code>init(int max_pendings, int max_poolsize)</code>
 * and <code>connect(String url)</code> must be called after the class was instanciated
 * .
 * User can make Non-Buffered request by requesting service with explicit Timeout value.
 *
 * User can mount their reponse handling function as the corresponding return path for
 * a particular request.
 *
 * To use this class, the JavaMail API (mailapi.jar) MUST be installed in your Java CLASSPATH accordingly.
 *
 * @author  William Choi <avextk@gmail.com>
 * @see     Named
 * @see Buffered
 * @see NetChannel
 *
 */
public class HttpChannel implements Named, Buffered, ChannelIntf {
    private static final int            DEFAULT_TIMEOUT     = 2000; //millis
    private AdaptiveAgedExecutorPool    thdpool;
    private CountedBlockingQueue        reqsque;
    private BufferingShim               bufshim;

    private int         timeout;
    private boolean     mltiplx;
    private boolean     ownpool;
    private String      insname;
    private String      baseurl;
    private DLogger     dlogger;
    private Numbering   seqget;
    private Numbering   seqpst;
    //private AdaptiveAgedExecutorPool    thpool;

    /**
     * Class constructor for creating the HttpChannel instance
     * @param given     The name of this instance which is given by the user who instanciates this class.
     */
    public HttpChannel(String given){
        insname = given;
        ownpool = false;
        mltiplx = false;
        baseurl = null;
        thdpool = null;
        reqsque = null;
        bufshim = null;
        timeout = DEFAULT_TIMEOUT;
        dlogger = DLogger.getInstance(this);
        reqsque = new CountedBlockingQueue<HttpRunnableRequest>();
        seqget  = new Numbering(1, Integer.MAX_VALUE);
        seqpst  = new Numbering(1, Integer.MAX_VALUE);
    }

    /**
     * <code>init()</code> is the MUST called init'er after the instance of this class was constructed.
     *
     * This version of init method will instanciate an internal Executor Pool for this instance.
     * If the instance has already been init'ed, no matter holding own Executor Pool, or referencing
     * external pool, an IllegalStateException will be thrown.
     *
     * @param max_pendings      The max queue size
     * @param max_poolsize      The max executor pool size
     * @throws IllegalStateException    while the internal Executor Pool was inited.
     */
    public void init(int max_pendings, int max_poolsize) throws IllegalStateException{
        if ((null != thdpool) && !thdpool.isTerminated())
            throw new IllegalStateException("This channel is referencing an active Executor Pool, no re-init is allowed.");

        AdaptiveAgedExecutorPool newpool = null;
        newpool  = new AdaptiveAgedExecutorPool(this.getName() + "-Internal-Pool");
        newpool.init(max_pendings, max_poolsize);

        thdpool = newpool;
        ownpool = true;
    }

    /**
     * <code>init()</code> is the MUST called init'er after the instance of this class was constructed.
     *
     * This version of init method will NOT instanciate any Executor Pool for this instance. It just
     * utilizes a pool provided from the outside world.
     *
     * If the instance is referencing an active Executor Pool, calling this method will throw an IllegalStateException
     *
     * @param pool_outside          The reference pointer of the outside pool
     * @throws NullPointerException     If pool_outside is NULL.
     * @throws IllegalStateException    If the instance is holding an active Executor Pool.
     */
    public void init(AdaptiveAgedExecutorPool pool_outside) throws NullPointerException, IllegalStateException {
        if (null == pool_outside)
            throw new NullPointerException("Reference pointer to the outside pool is NULL.");

        if ((null != thdpool) && !thdpool.isTerminated())
            throw new IllegalStateException("This channel is referencing an active Executor Pool, no re-init is allowed.");

        thdpool = pool_outside;
        ownpool = false;
    }

    
    /**
     * Public method for setting the base URL string for this HTTP ChannelIntf
     * @param url                      an absolute URL giving the base location of the web service.
     * @throws MalformedURLException    if the input url is not correct.
     * @throws IOException              if the service identified by the url doesn't exist.
     */
    @Override
    public void connect(String url) throws MalformedURLException, IOException{
        try {

            URL http_url = new URL(url);
            HttpURLConnection http_conn = (HttpURLConnection) http_url.openConnection();

            //Try access to foresee if this url is applicable. Exception will occur if access failed.
            http_conn.getResponseCode();

            //Save the url as the base url for this channel object
            baseurl = url;

            //TODO: CHECK IF THE DESTINED SERVER SUPPORTS MULTIPLEXED TRANSPORTATION

            //Create BufferingShim if MULTIPLEXING is supported
            if ((mltiplx) && (null == bufshim)){
                bufshim = new BufferingShim(32, 800);
                bufshim.bindTo(this);
            }

        }catch (MalformedURLException ex) {
            dlogger.warn(ex);
            throw ex;
        }catch (IOException ex) {
            dlogger.warn(ex);
            throw ex;
        }
    }

    /**
     * Method for getting the readiness of the instance
     * @return  True if the instance is ready, i.e. the base URL as well as the Executor Pool are set properly. False otherwise.
     */
    @Override
    public boolean isReady(){
        return ((null != baseurl) && (null != thdpool));
    }

    /**
     * Simple method for performing the HTTP GET by using the DEFAULT_TIMEOUT
     * The request is based on the channel instance's base url.
     * Request parameters are fed in the key-value pair Map in addition
     * This implementation is non-blocking design, user must mount in a response handler as request
     *
     * @param params    HTTP GET paramters, passing through using a Map of &lt;Key: String, Value any&gt;
     * @param handler   Implementation of interface NetChannelResponseHandler. It acts as the receiving callback.
     * @return  An instance of Cancellable interface representing a ticket of the request.
     * @throws IllegalStateException            If this channel is not yet ready.
     * @throws MalformedURLException            Some error happens in the params-constructed URL.
     * @throws IOException                      Connection I/O exceptions.
     */
    @Override
    public ChannelRequestIntf get(Map<Object, Object> params, ChannelResponseIntf handler, Object callbackID) throws IllegalStateException, MalformedURLException, IOException{
        return get(params, handler, callbackID, timeout);
    }

    
    /**
     * Method for performing the HTTP GET operation with user specified timeout value
     * @param   params    HTTP GET paramters, passing through using a Map of &lt;Key: String, Value any&gt;
     * @param   handler   Implementation of interface NetChannelResponseHandler. It acts as the receiving callback.
     * @param   timeout   The user specified timeout value of this request.
     * @return  An instance of Cancellable interface representing a ticket of the request.
     * @throws IllegalStateException            If this channel is not yet ready.
     * @throws MalformedURLException            Some error happens in the params-constructed URL.
     * @throws IOException                      Connection I/O exceptions.
     */
    @Override
    public ChannelRequestIntf get(Map<Object, Object> params, ChannelResponseIntf handler, Object callbackID, int timeout) throws IllegalStateException, MalformedURLException, IOException{
        if (!isReady())
            throw new IllegalStateException("No valid base URL or Executor Pool is set before request.");

        //Retrieve the parameters and construct the params string
        StringBuilder rqst_params = new StringBuilder();

        for (Map.Entry param : params.entrySet()) {
            Object key = param.getKey();
            Object val = param.getValue();
            if (val instanceof List){
                List<Object> vl = (List<Object>) val;
                for (Object v : vl){
                    StringBuilder param_string = new StringBuilder();
                    param_string.append(key).append("=").append(URLEncoder.encode(v.toString(), "utf8"));

                    //Append this key-value pair into the rqst_params
                    rqst_params.append("&").append(param_string);
                }
            }else{
                StringBuilder param_string = new StringBuilder();
                param_string.append(key).append("=").append(URLEncoder.encode(val.toString(), "utf8"));

                //Append this key-value pair into the rqst_params
                rqst_params.append("&").append(param_string);
            }

        }

        //Replace the leading '&' with '?'
        rqst_params.replace(0, 1, "?");

        try{
            //Prepare the HttpUrlConnection
            URL httpurl = new URL(baseurl + rqst_params);
            HttpURLConnection httpconn = (HttpURLConnection) httpurl.openConnection();

            //Set to use the HTTP GET method as well as Input & Output processing
            httpconn.setRequestMethod("GET");
            httpconn.setRequestProperty("Charset", "UTF-8");
            httpconn.setDoOutput(true);
            httpconn.setDoInput(true);
            
            //Instanciate an HttpRunnableRequest object
            HttpRunnableRequest request = new HttpRunnableRequest(httpconn, handler, callbackID, timeout);

            //Put the new request into the thread pool
            thdpool.submit(request);

            //Record the request to our internal queue
            reqsque.add(request);
            
            //Okay, we reach here means that everything is good, return the request as a Cancellable ticket
            return ((ChannelRequestIntf) request);
            
        }catch (MalformedURLException ex) {
            dlogger.warn(ex);
            throw ex;
        }catch (IOException ex) {
            dlogger.warn(ex);
            throw ex;
        }
    }

    /**
     * Method for performing HTTP POST operation by using the DEFAULT_TIMEOUT
     * Similar to the GET method, all the connection is based on the channel's base url.
     * All parameters / data will be carried in the payload in a MIME type format.
     * It currently supports only the HTML major MIME type - multipart/form-data.
     * This implementation is non-blocking design, so user must feed in a callback handler
     * for receving the POST response.
     *
     * planned features: multiple files / large file in multipart /
     * @param params    Parameters for the HTTP POST operation, passing through using a Map of &lt;Key: Object, Value: Object&gt;<br>
     *                      e.g. &lt;Key: File [file handler], Value: String [param name]&gt; means file uploading.<br>
     *                      e.g. &lt;Key: String [param name], Value: String [content]&gt; means plain text.
     * @param handler   Implementation of interface NetChannelResponseHandler. It acts as the receiving callback.
     * @return  An instance of Cancellable interface representing a ticket of the request.
     * @throws IllegalStateException            If this channel is not yet ready.
     * @throws MalformedURLException            Some error happens in the params-constructed URL.
     * @throws IOException                      Connection I/O exceptions.
     * @throws MessagingException               Exceptions while appending attachments in the Multipart/form-data format.
     */
    @Override
    public ChannelRequestIntf post(Map<Object, Object> params, ChannelResponseIntf handler, Object callbackID) throws IllegalStateException, MalformedURLException, IOException {
       return post(params, handler, callbackID, timeout);
    }

    /**
     * Method for performing HTTP POST operation.
     * Similar to the GET method, all the connection is based on the channel's base url.
     * All parameters / data will be carried in the payload in a MIME type format.
     * It currently supports only the HTML major MIME type - multipart/form-data.
     * This implementation is non-blocking design, so user must feed in a callback handler
     * for receving the POST response.
     *
     * planned features: multiple files / large file in multipart /
     * @param params    Parameters for the HTTP POST operation, passing through using a Map of &lt;Key: Object, Value: Object&gt;<br>
     *                      e.g. &lt;Key: File [file handler], Value: String [param name]&gt; means file uploading.<br>
     *                      e.g. &lt;Key: String [param name], Value: String [content]&gt; means plain text.
     * @param handler   Implementation of interface NetChannelResponseHandler. It acts as the receiving callback
     * @param timeout   The user specified timeout value of this request.
     * @return  An instance of Cancellable interface representing a ticket of the request.
     * @throws IllegalStateException            If this channel is not yet ready.
     * @throws MalformedURLException            Some error happens in the params-constructed URL.
     * @throws IOException                      Connection I/O exceptions.
     * @throws MessagingException               Exceptions while appending attachments in the Multipart/form-data format.
     */
    @Override
    public ChannelRequestIntf post(Map<Object, Object> params, ChannelResponseIntf handler,Object callbackID, int timeout) throws IllegalStateException, MalformedURLException, IOException {
        if (!isReady())
            throw new IllegalStateException("No valid base URL or Executor Pool is set before request.");
        
        try {
            //Instanciate an MimeMultipart as a container
            MimeMultipart multipart = new MimeMultipart();
            multipart.setSubType("form-data");

            //Iterates each parameter pair
            for (Map.Entry param : params.entrySet()) {
                //Get the key-value pair from the Iterator
                Object paramkey = param.getKey();
                Object paramval = param.getValue();

                //Construct the bodypart according to the type, then attach it to the Multipart
                MimeBodyPart mimebody = new MimeBodyPart();

                //If the paramkey is type of File, the paramval will be used as the key of the bodypart
                if (paramkey.getClass().equals(File.class)){
                    File file = (File) paramkey;
                    mimebody.setDisposition("file; name=" + (String)paramval);
                    mimebody.attachFile(file);
                    mimebody.setFileName(MimeUtility.encodeText(file.getName(), "utf8", "B")); //B means Base64
                    multipart.addBodyPart(mimebody);
                } else {
                    //Not a file, currently supports SetText only
                    String content = paramval.toString();
                    mimebody.setDisposition("form-data; name=" + paramkey);
                    mimebody.setText(MimeUtility.encodeText(content, "utf8", "B"));
                    multipart.addBodyPart(mimebody);
                }
            }

            //Prepare the HttpUrlConnection
            URL httpurl = new URL(baseurl);
            HttpURLConnection httpconn = (HttpURLConnection) httpurl.openConnection();

            //Sets the parameters
            setConnParams(httpconn, multipart.getContentType());

            //Attach the multipart to the connection stream
            multipart.writeTo(httpconn.getOutputStream());

            //Instanciate an HttpRunnableRequest object
            HttpRunnableRequest request = new HttpRunnableRequest(httpconn, handler, callbackID, timeout);

            //Put the new request into the thread pool
            thdpool.submit(request);

            //Record the request to our internal queue
            reqsque.add(request);
            
            //Okay, we reach here means that everything is good, return the request as a Cancellable ticket
            return ((ChannelRequestIntf) request);
            
        }catch (MalformedURLException ex) {
            dlogger.warn(ex);
            throw ex;
        }catch (IOException ex) {
            dlogger.warn(ex);
            throw ex;
        }catch (MessagingException ex) {
            dlogger.warn(ex);
            IOException newex = new IOException("MIME Messaging Exception");
            newex.setStackTrace(ex.getStackTrace());
            throw newex;
        }

    }

    /**
     * Method for user to disconnect from this channel. Calling this method will cause the
     * @param await_time
     * @param unit
     */
    @Override
    public void disconnect(long await_time, TimeUnit unit) throws InterruptedException{
        if (ownpool){
            thdpool.shutdown();
            thdpool.awaitTermination(await_time, unit);
        }

        while(!reqsque.isEmpty()){
            ((Cancellable) reqsque.fetch()).cancel();
        }
        baseurl = null;
    }

    @Override
    public long getRequestCount(){
        return reqsque.size();
    }
    
    @Override
    public String getName() {
        return insname;
    }

    /******************
     * Private Helpers
     */

    /**
     * Private method for processing the incoming request, including packing of request meta-data,
     * submission of non-explicit-timed request into BufferingShim, or directly instanciate runnable
     * request.
     *
     * @param params
     * @param handler
     * @param callbackID
     * @param timeout
     */
    private void processRequest(String method, Map<?, ?> params, ChannelResponseIntf handler, Object callbackID, int timeout){
        ChannelRequest reqst = new ChannelRequest(method, params, handler, callbackID, (0 == timeout ? DEFAULT_TIMEOUT : timeout));
        if (0 == timeout){
            bufshim.offer(reqst);
        }else{
            List single = new ArrayList();
            single.add(reqst);
            process(single);
        }
    }

    private void setConnParams(HttpURLConnection httpconn, String ctype){
        try {
            //Set to use the HTTP GET method as well as Input & Output processing
            httpconn.setRequestMethod("POST");
            httpconn.setRequestProperty("Charset", "UTF-8");
            httpconn.setRequestProperty("content-type", ctype);
            httpconn.setDoOutput(true);
            httpconn.setDoInput(true);
        } catch (ProtocolException ex) {
            dlogger.warn(ex);
        }
    }

    private static MimeBodyPart packBodyPart(String key, Object val) throws IOException, MessagingException{
        MimeBodyPart mimebody = new MimeBodyPart();
        
        mimebody.setDisposition("form-data; name=" + key);
        
        if (val instanceof File) {
            mimebody.attachFile((File) val);
            mimebody.setFileName(MimeUtility.encodeText(((File) val).getName(), "utf8", "B")); //B means Base64
        } else if (val instanceof MimeMultipart) {
            mimebody.setContent((MimeMultipart)val);
        } else {
            mimebody.setText(MimeUtility.encodeText(val.toString(), "utf8", "B"));
        }
        return mimebody;
    }

    private static String packQueryString(String key, Object val) throws UnsupportedEncodingException{
        StringBuilder urlqstr = new StringBuilder();
        urlqstr.append("&").append(key).append("=").append(URLEncoder.encode(val.toString(), "utf8"));

        return urlqstr.toString();
    }

    public static int modulateParams(Iterable<Entry<?, ?>> params, Object carrier) throws IllegalArgumentException {
        int partcount = 0;
        
        if (!(carrier instanceof MimeMultipart) && !(carrier instanceof StringBuilder))
            throw new IllegalArgumentException("Carrier MUST be either an instance of StringBuilder or MimeMultipart");

        for (Entry p : params){
            String paramkey     = (String) p.getKey();
            Object paramval     = p.getValue();
            Iterable valitrbl   = null;

            if (paramval.getClass().isArray()){
                valitrbl = new ArrayX(paramval);
            }else if (paramval instanceof Iterable){
                valitrbl = (Iterable) paramval;
            }else{
                //Wrap an single instance into a List
                List wrap = new ArrayList();
                wrap.add(paramval);
                valitrbl = wrap;
            }

            for (Object v : valitrbl){
                if (carrier instanceof MimeMultipart){
                    try {
                        MimeMultipart mimemulti = (MimeMultipart) carrier;
                        mimemulti.addBodyPart(packBodyPart(paramkey, v));
                        partcount++;
                    } catch (IOException ex) {
                        //dlogger.warn(ex);
                    } catch (MessagingException ex) {
                        //dlogger.warn(ex);
                    }

                }else {
                    try {
                        StringBuilder urlquery = (StringBuilder) carrier;
                        urlquery.append(packQueryString(paramkey, v));
                        partcount++;
                    } catch (UnsupportedEncodingException ex) {
                        //dlogger.warn(ex);
                    }

                }
            }//End-of-iterating value list of a param

        } //End-of-iterating the params list

        return partcount;
    }

    public static void demuxRequests(){
        
    }

    public static HttpURLConnection muxRequests(Iterable requests){
        HttpURLConnection               httpconn = null;
        Map<Integer, ChannelRequest>    mapmuxed = null;
        boolean using_post = false;
        int     muxed_gets = 0;
        int     muxed_psts = 0;
        int     muxed_reqs = 0;

        mapmuxed    = new HashMap<Integer, ChannelRequest>();

        MimeMultipart   multi = new MimeMultipart();
        StringBuilder   urlqr = new StringBuilder();

        //Iterate all buffered requests
        for (Object req : requests){
            if (!(req instanceof ChannelRequest))
                continue;

            //Cast to the type we would process
            ChannelRequest chnlreq = (ChannelRequest) req;

            if (chnlreq.getMethod().equalsIgnoreCase("POST")){
                try {
                    MimeMultipart reqparams = new MimeMultipart();
                    int ret = modulateParams(chnlreq, reqparams);
                    if (ret > 0){
                        multi.addBodyPart(packBodyPart("__REQ_" + muxed_reqs + "__", reqparams));
                        mapmuxed.put(muxed_reqs, chnlreq);
                        muxed_psts++;
                        muxed_reqs++;
                        using_post = true;
                    }                   
                } catch (IOException ex) {
                    //dlogger.warn(ex);
                } catch (MessagingException ex) {
                    //dlogger.warn(ex);
                }
            }else{
                StringBuilder reqstr = new StringBuilder();
                int ret = modulateParams(chnlreq, reqstr);
                if (ret > 0){
                    urlqr.append(reqstr.toString().replaceAll("&", "&__REQ_" + muxed_reqs +"__."));
                    mapmuxed.put(muxed_reqs, chnlreq);
                    muxed_gets++;
                    muxed_reqs++;
                }
            }
        }


        try {
            multi.writeTo(System.out);
            System.out.println("URL:: " + urlqr);            
        } catch (IOException ex) {
            Logger.getLogger(HttpChannel.class.getName()).log(Level.SEVERE, null, ex);
        } catch (MessagingException ex) {
            Logger.getLogger(HttpChannel.class.getName()).log(Level.SEVERE, null, ex);
        }

        //Construct RunnableRequest
        //Put Map of muxed into RunnableRequest
        //Instanciate URLConn with base_url, query str, multipart form-data
        //Put URLConn into RunnableRequest
        //
        
        return httpconn;
    }

    @Override
    public void flush() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void process(Iterable requests) {
        //new RunnableRequest()

    }

}
